/*
 * JSF Toolkit Component Framework
 * Copyright (C) 2007 Noah Sloan <iamnoah A-T gmail D0T com>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 */
package com.jsftoolkit.gen.writer.impl;

import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.faces.component.UIComponent;

import com.jsftoolkit.gen.writer.ClassWriter;
import com.jsftoolkit.utils.Utils;

/**
 * Basic implementation of a ClassWriter.
 * 
 * @author noah
 * 
 */
public class BasicClassWriter implements ClassWriter {

	private static final String FOO = "foo";

	public static final String DEFAULT_TAB = "    ";

	public static final int CONSTANT = Modifier.PUBLIC | Modifier.STATIC
			| Modifier.FINAL;

	public static final String WARNING = "/**\n"
			+ " * **GENERATED SOURCE**\n"
			+ " * You may subclass this class, but should probably not edit it.\n"
			+ " * See http://www.jsftoolkit.com for more information.\n"
			+ " */";

	protected PrintStream out;

	protected Set<Class<?>> imports = new HashSet<Class<?>>();

	protected String _package;

	protected String className;

	private String tab = DEFAULT_TAB;

	private int indentLevel;

	public BasicClassWriter() {
		super();
	}

	public BasicClassWriter(final PrintStream out) {
		super();
		this.out = out;
	}

	/**
	 * Prints nothing.
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#printFileComment()
	 */
	public void printFileComment() {
		// print nothing
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#printPackage(java.lang.String)
	 */
	public void printPackage(String _package) {
		this._package = _package;
		out.printf("package %s;\n\n", _package);
	}

	/**
	 * Prints the imports in lexigraphic order.
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#printImports(java.util.Set)
	 * @see #printImport(Class)
	 */
	public void printImports(Set<Class<?>> imports) {
		// print in lexigraphic order
		List<Class<?>> list = new ArrayList<Class<?>>(imports);
		Collections.sort(list, new Comparator<Class<?>>() {
			public int compare(Class<?> o1, Class<?> o2) {
				return o1.getCanonicalName().compareTo(o2.getCanonicalName());
			}
		});
		for (Class<?> _import : list) {
			printImport(_import);
		}
		out.println();
	}

	/**
	 * Prints an import statement for the given class, only if the class needs
	 * to be imported according to {@link #needsQualification(Class)}. Also
	 * records that the class has been imported.
	 * 
	 * @param _import
	 */
	protected void printImport(Class<?> _import) {
		if (needsQualification(_import)) {
			out.printf("import %s;\n", _import.getCanonicalName());
			this.imports.add(_import);
		}
	}

	/**
	 * Determines if references to the given class need to be fully qualified.
	 * 
	 * @param _import
	 * @return true if import (or its component, in case of an array) is not a
	 *         primitive type, is in a different package, is not in java.lang,
	 *         and has not been imported. Otherwise, false.
	 */
	protected boolean needsQualification(Class<?> _import) {
		return _import.isArray() ? needsQualification(_import
				.getComponentType()) : !packageEquals(_import.getPackage(),
				"java.lang")
				&& !packageEquals(_import.getPackage(), _package)
				&& !_import.isPrimitive() && !imports.contains(_import);
	}

	/**
	 * Null safe equals for package names.
	 * 
	 * @param p
	 * @param name
	 * @return true if p.getName().equals(name) or name is null and either p is
	 *         null or p.getName() is null.
	 */
	protected boolean packageEquals(Package p, String name) {
		if (p == null) {
			return name == null;
		}
		return Utils.equals(p.getName(), name);
	}

	/**
	 * This one prints a warning not to alter the code, since it is generated.
	 * 
	 * @see com.jsftoolkit.gen.writer.ClassWriter#printClassComment()
	 */
	public void printClassComment() {
		out.println(WARNING);
	}

	/**
	 * 
	 * @see com.jsftoolkit.gen.writer.ClassWriter#startField(int,
	 *      java.lang.Class, java.lang.String)
	 */
	public void startField(int mods, Class<?> type, String name) {
		indent();
		printModifiers(mods);
		printType(type);
		out.printf(" %s", name);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#printConstant(java.lang.Class,
	 *      java.lang.String, java.lang.String)
	 */
	public void printConstant(Class<?> type, String name, CharSequence value) {
		startField(CONSTANT, type, name);
		out.printf(" = %s;\n\n", value);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#printDeclaration(int,
	 *      java.lang.String, java.lang.Class, java.lang.Iterable)
	 */
	public void printDeclaration(int mods, String className,
			Class<?> superClass, Iterable<Class<?>> interfaces) {
		printModifiers(mods);
		out.printf("class %s", className);

		this.className = className;

		// superclass
		if (superClass != null) {
			out.print(" extends ");
			printType(superClass);
		}

		// interfaces
		Iterator<Class<?>> it;
		if (interfaces != null && (it = interfaces.iterator()).hasNext()) {
			out.print(" implements ");
			printType(it.next());
			while (it.hasNext()) {
				out.print(", ");
				printType(it.next());
			}
		}
		out.print(' ');
		startBlock();
		out.println();
	}

	/**
	 * @see PrintStream#printf(String, Object[])
	 * 
	 * @param format
	 * @param args
	 */
	public ClassWriter printf(String format, Object... args) {
		out.printf(format, args);
		return this;
	}

	public ClassWriter print(char c) {
		out.print(c);
		return this;
	}

	public ClassWriter print(CharSequence s) {
		out.print(s);
		return this;
	}

	public ClassWriter println(CharSequence s) {
		out.println(s);
		return this;
	}

	/**
	 * @see com.jsftoolkit.gen.writer.ClassWriter#printModifiers(int)
	 */
	public void printModifiers(int mod) {
		if (Modifier.isPublic(mod)) {
			out.print("public ");
		} else if (Modifier.isProtected(mod)) {
			out.print("protected ");
		} else if (Modifier.isPrivate(mod)) {
			out.print("private ");
		}

		if (Modifier.isTransient(mod)) {
			out.print("transient ");
		}
		if (Modifier.isVolatile(mod)) {
			out.print("volatile ");
		}
		if (Modifier.isNative(mod)) {
			out.print("native ");
		}
		if (Modifier.isSynchronized(mod)) {
			out.print("synchronized ");
		}
		if (Modifier.isStatic(mod)) {
			out.print("static ");
		}

		if (Modifier.isFinal(mod)) {
			out.print("final ");
		} else if (Modifier.isAbstract(mod)) {
			out.print("abstract ");
		}

	}

	/**
	 * Simple delegates to
	 * {@link #startMethod(int, Class, String, LinkedHashMap)}.
	 * 
	 * @see com.jsftoolkit.gen.writer.ClassWriter#startConstructor(int,
	 *      java.util.LinkedHashMap)
	 * 
	 */
	public void startConstructor(int mods,
			LinkedHashMap<String, Class<?>> params,
			Class<? extends Throwable>... exceptions) {
		startMethod(mods, null, className, params, exceptions);
	}

	/**
	 * Does not provide a method comment.
	 * 
	 * @see com.jsftoolkit.gen.writer.ClassWriter#startMethod(int,
	 *      java.lang.Class, java.lang.String, java.util.LinkedHashMap)
	 */
	public void startMethod(int mods, Class<?> returnType, String name,
			LinkedHashMap<String, Class<?>> params,
			Class<? extends Throwable>... exceptions) {
		indent();
		printModifiers(mods);
		if (returnType != null) {
			printType(returnType);
			print(' ');
		}
		out.printf("%s(", name);
		if (params != null) {
			printParameters(params);
		}
		out.print(") ");
		if (exceptions != null && exceptions.length > 0) {
			out.print("throws");
			for (int i = 0; i < exceptions.length;) {
				print(' ');
				printType(exceptions[i]);
				if (++i < exceptions.length) {
					out.print(',');
				}
			}
			print(' ');
		}
		startBlock();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#endBlock()
	 */
	public void endBlock() {
		decIndent();
		newLine();
		out.println("}\n");
	}

	/**
	 * Prints a parameter list, e.g. "int mods, Class returnType, String name"
	 * 
	 * @param params
	 */
	protected void printParameters(LinkedHashMap<String, Class<?>> params) {
		Iterator<Entry<String, Class<?>>> it = params.entrySet().iterator();
		while (it.hasNext()) {
			Entry<String, Class<?>> next = it.next();
			out.print("final ");
			printType(next.getValue());
			out.print(' ');
			out.print(next.getKey());
			if (it.hasNext()) {
				out.print(", ");
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#printType(java.lang.Class)
	 */
	public void printType(Class<?> c) {
		if (c == null) {
			return;
		}
		if (c == Void.class) {
			out.print("void");
			return;
		}
		if (c.isAnonymousClass()) {
			// anonymous means it was parameterized
			Type type;
			if (TypeParameter.class.isAssignableFrom(c)) {
				// if it is a TypeParameter, get the argument
				type = (((ParameterizedType) c.getGenericInterfaces()[0])
						.getActualTypeArguments()[0]);
			} else {
				type = c.getGenericSuperclass();
			}
			if (type instanceof ParameterizedType) {
				printParameterizedType((ParameterizedType) type);
			} else {
				// otherwise it's an anonymous interface implementation
				Type[] interfaces = c.getGenericInterfaces();
				if (interfaces.length < 1) {
					throw new IllegalArgumentException("Type " + c
							+ " is anonymous but not parameterized.");
				}
				assert interfaces.length == 1;
				printParameterizedType((ParameterizedType) interfaces[0]);
			}
		} else {
			// otherwise just print the class name
			out.print(getClassName(c));
		}
	}

	/**
	 * Prints a {@link ParameterizedType}. Delegates to
	 * {@link #getClassName(Class)} to resolve the actual class names.
	 * 
	 * @param pt
	 */
	protected void printParameterizedType(ParameterizedType pt) {
		Class<?> rawType = (Class<?>) pt.getRawType();
		Type[] typeArgs = pt.getActualTypeArguments();
		out.print(getClassName(rawType));
		out.print('<');
		for (int i = 0; i < typeArgs.length; i++) {
			// recursively print each type argument
			if (typeArgs[i] instanceof ParameterizedType) {
				printParameterizedType((ParameterizedType) typeArgs[i]);
			} else if (typeArgs[i] instanceof WildcardType) {
				WildcardType wt = (WildcardType) typeArgs[i];
				printWildcardType(wt);
			} else {
				printType((Class<?>) typeArgs[i]);
			}
			if (i + 1 < typeArgs.length) {
				out.print(',');
			}
		}
		out.print('>');
	}

	protected void printWildcardType(WildcardType wt) {
		Type[] lowerBounds = wt.getLowerBounds();
		Type[] upperBounds = wt.getUpperBounds();
		for (int i = 0; i < upperBounds.length || i < lowerBounds.length; i++) {
			Type lower = Utils.get(lowerBounds, i);
			Type upper = Utils.get(upperBounds, i);
			print("?");
			if (lower == null) {
				print(" extends ");
				printType((Class<?>) upper);
			} else if (upper != null) {
				print(" super ");
				printType((Class<?>) lower);
			}
		}
	}

	/**
	 * Determines how the class name for the given class should be printed.
	 * 
	 * @param c
	 *            the class to check
	 * @return the canonical name if {@link #needsQualification(Class)} returns
	 *         true, otherwise the simple name.
	 */
	protected String getClassName(Class<?> c) {
		return needsQualification(c) ? c.getCanonicalName() : c.getSimpleName();
	}

	@SuppressWarnings("serial")
	public void printPropertyAccessors(final String propertyName,
			final Class<?> propertyClass) {
		String capitalized = Utils.capitalize(propertyName);
		final String fieldName = getFieldName(propertyName);
		// write the getter
		startMethod(Modifier.PUBLIC, propertyClass, "get" + capitalized, null);
		indent().printf("return %s;", fieldName).endBlock();

		// write the setter
		startMethod(Modifier.PUBLIC, void.class, "set" + capitalized,
				new LinkedHashMap<String, Class<?>>() {
					{
						put(fieldName, propertyClass);
					}
				});
		indent().printf("this.%s = %1$s;", fieldName).endBlock();
	}

	/**
	 * Prepends an underscore.
	 */
	public String getFieldName(String name) {
		return '_' + name;
	}

	/**
	 * Prints '{\n' and increases the indent.
	 * 
	 * @see com.jsftoolkit.gen.writer.ClassWriter#openBlock()
	 */
	public void startBlock() {
		out.println('{');
		incIndent();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.jsftoolkit.gen.ClassWriter#close()
	 */
	public void close() {
		out.println('}');
		out.close();
	}

	/**
	 * 
	 * @return the current indent level
	 */
	protected int getIndent() {
		return indentLevel;
	}

	/**
	 * Set to -1 to disable auto-indenting.
	 * 
	 * @param indent
	 */
	protected void setIndent(int indent) {
		this.indentLevel = indent;
	}

	/**
	 * @see com.jsftoolkit.gen.writer.ClassWriter#indent()
	 * 
	 * (no effect if indent &lt; 0)
	 */
	public ClassWriter indent() {
		indent(indentLevel);
		return this;
	}

	/**
	 * Ends the current line and indents according to the current indent level.
	 */
	protected void newLine() {
		out.println();
		indent();
	}

	/**
	 * Indents to the given level.
	 * 
	 * @param level
	 */
	protected void indent(int level) {
		for (int i = 0; i < level; i++) {
			out.print(tab);
		}
	}

	/**
	 * Increases the indent level by one.
	 */
	protected void incIndent() {
		if (indentLevel >= 0) {
			indentLevel++;
		}
	}

	/**
	 * Decreases the indent level by 1 (no effect if indent is already 0).
	 */
	protected void decIndent() {
		if (indentLevel > 0) {
			indentLevel--;
		}
	}

	/**
	 * 
	 * @return the character sequence being used for indentation. Defaults to 4
	 *         spaces.
	 */
	public String getTab() {
		return tab;
	}

	public void setTab(String tab) {
		this.tab = tab;
	}

	public String getPackage() {
		return _package;
	}

	public void setPackage(String _package) {
		this._package = _package;
	}

	public String getClassName() {
		return className;
	}

	public void setClassName(String className) {
		this.className = className;
	}

	public Set<Class<?>> getImports() {
		return imports;
	}

	public void setImports(Set<Class<?>> imports) {
		this.imports = imports;
	}

	public PrintStream getOut() {
		return out;
	}

	public void setOut(PrintStream out) {
		this.out = out;
	}

	@SuppressWarnings("serial")
	public static void main(String[] args) {
		ClassWriter w = new BasicClassWriter(System.out);
		w.printPackage(FOO);
		w.printImports(new HashSet<Class<?>>() {
			{
				add(UIComponent.class);
				add(Comparator.class);
				add(Map.class);
			}
		});

		w.printClassComment();

		w.printDeclaration(Modifier.PUBLIC | Modifier.FINAL, "Bar",
				new HashMap<String, Class<? super UIComponent>>() {
				}.getClass(), new HashSet<Class<?>>() {
					{
						add(Comparator.class);
						add(Comparable.class);
					}
				});

		w.printConstant(Class.class, "FOO", "Foo.class");

		w.startField(Modifier.FINAL | Modifier.PROTECTED | Modifier.TRANSIENT,
				new TypeParameter<Map<? extends String, ? super Object>>() {
				}.getClass(), w.getFieldName(FOO));
		w.println(";\n");

		w.startConstructor(Modifier.PROTECTED,
				new LinkedHashMap<String, Class<?>>() {
					{
						put(FOO, Array.newInstance(String.class, 0).getClass());
					}
				});
		w.indent().printf("this.%s = %1$s;", w.getFieldName(FOO)).endBlock();

		w.printPropertyAccessors(FOO, String.class);

		w.close();
	}
}
